package com.wangyu.utils;

import com.wangyu.builder.TypedMapBuilder;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.MapUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpEntity;
import org.apache.http.NameValuePair;
import org.apache.http.StatusLine;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.entity.mime.HttpMultipartMode;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.ssl.SSLContextBuilder;
import org.apache.http.util.EntityUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.util.StreamUtils;

import javax.net.ssl.SSLContext;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.UnknownHostException;
import java.nio.charset.Charset;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.function.Consumer;
import java.util.stream.Collectors;

/**
 * Rest请求客户端
 *
 * @author Mr.Wang
 * <p>
 * @apiNote 1. 全builder调用，用户系统内部相互通信
 * 2. 支持异步回调
 * 3. 多样性组合
 * 4. 解耦实现
 * 5. 支持上传文件、FormData，JSON、支持自定义无侵入扩展
 */
public final class RestClient {

    private static final Logger logger = LogManager.getLogger(RestClient.class);
    /**
     * 请求配置
     */
    private static final RequestConfig REQUEST_CONFIG = RequestConfig.custom().setConnectTimeout(3000).build();
    /**
     * 请求解析器
     */
    private static final Map<HttpMethod, HttpMethodResolver> RESOLVER_MAP = resolverBuilder()
            .with(HttpMethod.GET, new HttpGetResolver())
            .with(HttpMethod.POST, new HttpPostResolver())
            .build();
    /**
     * 线程池
     */
    private static final ExecutorService DEFAULT_EXECUTOR = ThreadPoolManager.defaultCachedThreadPool();
    /**
     * 后缀映射
     */
    private static final Map<String, String> MINE_MAP = TypedMapBuilder.stringMapBuilder()
            .with("ai", "application/postscript")
            .with("aif", "audio/x-aiff")
            .with("aifc", "audio/x-aiff")
            .with("aiff", "audio/x-aiff")
            .with("asc", "text/plain")
            .with("au", "audio/basic")
            .with("avi", "video/x-msvideo")
            .with("bcpio", "application/x-bcpio")
            .with("bin", "application/octet-stream")
            .with("c", "text/plain")
            .with("cc", "text/plain")
            .with("ccad", "application/clariscad")
            .with("cdf", "application/x-netcdf")
            .with("class", "application/octet-stream")
            .with("cpio", "application/x-cpio")
            .with("cpt", "application/mac-compactpro")
            .with("csh", "application/x-csh")
            .with("css", "text/css")
            .with("dcr", "application/x-director")
            .with("dir", "application/x-director")
            .with("dms", "application/octet-stream")
            .with("doc", "application/msword")
            .with("drw", "application/drafting")
            .with("dvi", "application/x-dvi")
            .with("dwg", "application/acad")
            .with("dxf", "application/dxf")
            .with("dxr", "application/x-director")
            .with("eps", "application/postscript")
            .with("etx", "text/x-setext")
            .with("exe", "application/octet-stream")
            .with("ez", "application/andrew-inset")
            .with("f", "text/plain")
            .with("f90", "text/plain")
            .with("fli", "video/x-fli")
            .with("gif", "image/gif")
            .with("gtar", "application/x-gtar")
            .with("gz", "application/x-gzip")
            .with("h", "text/plain")
            .with("hdf", "application/x-hdf")
            .with("hh", "text/plain")
            .with("hqx", "application/mac-binhex40")
            .with("htm", "text/html")
            .with("html", "text/html")
            .with("ice", "x-conference/x-cooltalk")
            .with("ief", "image/ief")
            .with("iges", "model/iges")
            .with("igs", "model/iges")
            .with("ips", "application/x-ipscript")
            .with("ipx", "application/x-ipix")
            .with("jpe", "image/jpeg")
            .with("jpeg", "image/jpeg")
            .with("jpg", "image/jpeg")
            .with("js", "application/x-javascript")
            .with("kar", "audio/midi")
            .with("latex", "application/x-latex")
            .with("lha", "application/octet-stream")
            .with("lsp", "application/x-lisp")
            .with("lzh", "application/octet-stream")
            .with("m", "text/plain")
            .with("man", "application/x-troff-man")
            .with("me", "application/x-troff-me")
            .with("mesh", "model/mesh")
            .with("mid", "audio/midi")
            .with("midi", "audio/midi")
            .with("mif", "application/vnd.mif")
            .with("mime", "www/mime")
            .with("mov", "video/quicktime")
            .with("movie", "video/x-sgi-movie")
            .with("mp2", "audio/mpeg")
            .with("mp3", "audio/mpeg")
            .with("mp4", "video/mpeg")
            .with("mpe", "video/mpeg")
            .with("mpeg", "video/mpeg")
            .with("mpg", "video/mpeg")
            .with("mpga", "audio/mpeg")
            .with("ms", "application/x-troff-ms")
            .with("msh", "model/mesh")
            .with("nc", "application/x-netcdf")
            .with("oda", "application/oda")
            .with("pbm", "image/x-portable-bitmap")
            .with("pdb", "chemical/x-pdb")
            .with("pdf", "application/pdf")
            .with("pgm", "image/x-portable-graymap")
            .with("pgn", "application/x-chess-pgn")
            .with("png", "image/png")
            .with("pnm", "image/x-portable-anymap")
            .with("pot", "application/mspowerpoint")
            .with("ppm", "image/x-portable-pixmap")
            .with("pps", "application/mspowerpoint")
            .with("ppt", "application/mspowerpoint")
            .with("ppz", "application/mspowerpoint")
            .with("pre", "application/x-freelance")
            .with("prt", "application/pro_eng")
            .with("ps", "application/postscript")
            .with("qt", "video/quicktime")
            .with("ra", "audio/x-realaudio")
            .with("ram", "audio/x-pn-realaudio")
            .with("ras", "image/cmu-raster")
            .with("rgb", "image/x-rgb")
            .with("rm", "audio/x-pn-realaudio")
            .with("roff", "application/x-troff")
            .with("rpm", "audio/x-pn-realaudio-plugin")
            .with("rtf", "text/rtf")
            .with("rtx", "text/richtext")
            .with("scm", "application/x-lotusscreencam")
            .with("set", "application/set")
            .with("sgm", "text/sgml")
            .with("sgml", "text/sgml")
            .with("sh", "application/x-sh")
            .with("shar", "application/x-shar")
            .with("silo", "model/mesh")
            .with("sit", "application/x-stuffit")
            .with("skd", "application/x-koan")
            .with("skm", "application/x-koan")
            .with("skp", "application/x-koan")
            .with("skt", "application/x-koan")
            .with("smi", "application/smil")
            .with("smil", "application/smil")
            .with("snd", "audio/basic")
            .with("sol", "application/solids")
            .with("spl", "application/x-futuresplash")
            .with("src", "application/x-wais-source")
            .with("step", "application/STEP")
            .with("stl", "application/SLA")
            .with("stp", "application/STEP")
            .with("sv4cpio", "application/x-sv4cpio")
            .with("sv4crc", "application/x-sv4crc")
            .with("swf", "application/x-shockwave-flash")
            .with("t", "application/x-troff")
            .with("tar", "application/x-tar")
            .with("tcl", "application/x-tcl")
            .with("tex", "application/x-tex")
            .with("texi", "application/x-texinfo")
            .with("texinfo", "application/x-texinfo")
            .with("tif", "image/tiff")
            .with("tiff", "image/tiff")
            .with("tr", "application/x-troff")
            .with("tsi", "audio/TSP-audio")
            .with("tsp", "application/dsptype")
            .with("tsv", "text/tab-separated-values")
            .with("txt", "text/plain")
            .with("unv", "application/i-deas")
            .with("ustar", "application/x-ustar")
            .with("vcd", "application/x-cdlink")
            .with("vda", "application/vda")
            .with("viv", "video/vnd.vivo")
            .with("vivo", "video/vnd.vivo")
            .with("vrml", "model/vrml")
            .with("wav", "audio/x-wav")
            .with("wrl", "model/vrml")
            .with("xbm", "image/x-xbitmap")
            .with("xlc", "application/vnd.ms-excel")
            .with("xll", "application/vnd.ms-excel")
            .with("xlm", "application/vnd.ms-excel")
            .with("xls", "application/vnd.ms-excel")
            .with("xlw", "application/vnd.ms-excel")
            .with("xml", "text/xml")
            .with("xpm", "image/x-xpixmap")
            .with("xwd", "image/x-xwindowdump")
            .with("xyz", "chemical/x-pdb")
            .with("zip", "application/zip ")
            .with("apk", "application/vnd.android.package-archive")
            .with("*", "application/octet-stream")
            .build();

    private HttpRequestBase request;

    private boolean async = false;

    private Consumer<HttpEntity> consumer;

    private Consumer<RestClientException> errorConsumer;

    private ExecutorService executorService;

    private ResponseType responseType = ResponseType.NORMAL;


    /**
     * 内部构造方法，不对外公开
     *
     * @param request 请求信息
     */
    private RestClient(HttpRequestBase request) {
        this.request = request;
    }

    public static RestClientBuilder create() {
        return new RestClientBuilder();
    }

    private static TypedMapBuilder<HttpMethod, HttpMethodResolver> resolverBuilder() {
        return TypedMapBuilder.builder();
    }

    /**
     * 设置请求失败时的回调
     *
     * @param errorConsumer 错误回调
     * @return 结果
     */
    public RestClient onError(Consumer<RestClientException> errorConsumer) {
        this.errorConsumer = errorConsumer;
        return this;
    }

    /**
     * 错误处理
     *
     * @param e 异常
     */
    private void handleError(RestClientException e) {
        if (null != errorConsumer) {
            errorConsumer.accept(e);
        } else {
            throw e;
        }
    }

    /**
     * 设置响应类型
     *
     * @param responseType 响应类型
     * @return 结果
     */
    public RestClient responseType(ResponseType responseType) {
        this.responseType = responseType;
        return this;
    }

    /**
     * 标记线程池执行
     *
     * @return 结果
     */
    public RestClient async() {
        this.async = true;
        this.executorService = DEFAULT_EXECUTOR;
        return this;
    }

    /**
     * 标记指定线程池执行
     *
     * @param executorService 线程池
     * @return 结果
     */
    public RestClient async(ExecutorService executorService) {
        this.async = true;
        this.executorService = executorService;
        return this;
    }

    /**
     * 异步执行，接收结果
     *
     * @param consumer 结果
     */
    public void execute(Consumer<HttpEntity> consumer) {
        this.consumer = consumer;
        if (this.async) {
            if (this.executorService == null) {
                handleError(new RestClientException("线程池未指定或为空！"));
            }
            this.executorService.submit(this::executeSafety);
        } else {
            executeSafety();
        }
    }

    /**
     * 执行请求，返回Map
     *
     * @return map
     * @throws IOException 异常
     */
    public Map<String, Object> executeForMap() throws IOException {
        this.responseType = ResponseType.JSON;
        return innerExecute();
    }

    /**
     * 执行请求，返回字符串
     *
     * @return 字符串
     * @throws IOException 异常
     */
    public String executeForString() throws IOException {
        this.responseType = ResponseType.TEXT;
        return innerExecute();
    }

    /**
     * 安全的执行
     */
    private void executeSafety() {
        try {
            execute();
        } catch (IOException e) {
            handleError(new RestClientException(e.getMessage(), e, null));
        }
    }

    /**
     * 执行请求，返回响应实体，自行处理
     *
     * @return 响应实体
     * @throws IOException 异常
     */
    public <T> T execute() throws IOException {
        return innerExecute();
    }

    /**
     * 内部执行方法，预处理结果
     *
     * @param <T> 泛型
     * @return 结果
     */
    private <T> T innerExecute() throws IOException {
        try (CloseableHttpClient client = getClient();
             CloseableHttpResponse response = client.execute(request)) {
            StatusLine statusLine = response.getStatusLine();
            if (200 == statusLine.getStatusCode()) {
                HttpEntity entity = response.getEntity();
                if (consumer != null) {
                    consumer.accept(entity);
                }
                return resolveResponse(entity);
            } else {
                int requestCode = statusLine.getStatusCode();
                logger.error(request.getURI() + "接口调用失败，code:" + requestCode);
                handleError(new RestClientException("网络请求状态异常！代码：" + requestCode, null, statusLine));
            }
        } catch (UnknownHostException e) {
            handleError(new RestClientException(e.getMessage(), e, null));
            logger.error("未知的请求地址！");
        } finally {
            request.releaseConnection();
        }
        return null;
    }

    /**
     * 解析结果
     *
     * @param entity 响应体
     * @param <T>    泛型
     * @return 结果
     */
    @SuppressWarnings("unchecked")
    private <T> T resolveResponse(HttpEntity entity) throws IOException {
        switch (responseType) {
            case NORMAL:
                return (T) entity;
            case TEXT:
                return (T) EntityUtils.toString(entity);
            case JSON:
                return (T) ObjectParser.parseMap(EntityUtils.toString(entity));
            case BINARY:
                try (InputStream in = entity.getContent()) {
                    return (T) StreamUtils.copyToByteArray(in);
                }
            default:
                return (T) entity;
        }
    }

    /**
     * 获取请求客户端
     *
     * @return 结果
     */
    private CloseableHttpClient getClient() {
        return request.getURI().getScheme().equals("https") ? createSSLClient() : HttpClients.createDefault();
    }

    /**
     * 不信任的证书请求客户端
     *
     * @return 结果
     */
    private CloseableHttpClient createSSLClient() {
        //信任所有
        try {
            SSLContext context = SSLContextBuilder.create().loadTrustMaterial(null, (arg0, arg1) -> true).build();
            SSLConnectionSocketFactory factory = new SSLConnectionSocketFactory(context);
            return HttpClients.custom().setSSLSocketFactory(factory).build();
        } catch (NoSuchAlgorithmException | KeyManagementException | KeyStoreException e) {
            e.printStackTrace();
        }
        return null;
    }

    public enum HttpMethod {
        GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE;
    }

    /**
     * 响应类型
     */
    private enum ResponseType {
        NORMAL, TEXT, JSON, BINARY
    }

    /**
     * Http请求解析器
     */
    private interface HttpMethodResolver {
        HttpRequestBase resolve(RestClientBuilder builder);
    }

    /**
     * 异常类，用于包装异常
     */
    public static class RestClientException extends RuntimeException {

        private static final long serialVersionUID = 4741281547788724661L;
        private Exception nested;

        private Object bind;

        public RestClientException(String message, Exception nested) {
            super(message);
            this.nested = nested;
        }

        public RestClientException(String message, Exception nested, Object bind) {
            super(message);
            this.nested = nested;
            this.bind = bind;
        }

        public RestClientException(String message) {
            super(message);
        }

        @SuppressWarnings("unchecked")
        public <T> T getBind() {
            return (T) bind;
        }

        public void setBind(Object bind) {
            this.bind = bind;
        }

        public Exception getNested() {
            return nested;
        }
    }

    /**
     * 主要的builder，核心构建
     */
    public static class RestClientBuilder {

        private String url;

        private HttpMethod method = HttpMethod.GET;

        private Map<String, Object> params;

        private String body;

        private Map<String, String> headers;

        private List<Multipart> multipartList;

        private boolean multipart;

        private String charset;

        public String getUrl() {
            return url;
        }

        public RestClientBuilder url(String url) {
            this.url = url;
            return this;
        }

        public HttpMethod getMethod() {
            return method;
        }

        public RestClientBuilder method(HttpMethod method) {
            this.method = method;
            return this;
        }

        public RestClientBuilder get() {
            this.method = HttpMethod.GET;
            return this;
        }

        public RestClientBuilder post() {
            this.method = HttpMethod.POST;
            return this;
        }

        public RestClientBuilder multipart() {
            this.multipart = true;
            return this;
        }

        public boolean isMultipart() {
            return multipart;
        }

        public Map<String, Object> getParams() {
            if (null == params) {
                params = new HashMap<>();
            }
            return params;
        }

        public RestClientBuilder queryParams(Map<String, Object> params) {
            this.params = params;
            return this;
        }

        public RestClientBuilder addParam(String key, Object value) {
            if (null == this.params) {
                this.params = new HashMap<>();
            }
            this.params.put(key, value);
            return this;
        }

        public RestClientBuilder charset(String charset) {
            this.charset = charset;
            return this;
        }

        public Charset getCharset() {
            return StringUtils.isBlank(charset) ? Charset.defaultCharset() : Charset.forName(charset);
        }

        public RestClientBuilder addMultipartBody(String name, String filename, Object data) {
            if (null == this.multipartList) {
                this.multipartList = new ArrayList<>();
            }
            this.multipartList.add(new Multipart(name, filename, data));
            return this;
        }

        public List<Multipart> getMultipartList() {
            if (null == multipartList) {
                multipartList = new ArrayList<>();
            }
            return multipartList;
        }

        public String getBody() {
            return body;
        }

        public RestClientBuilder body(String body) {
            this.body = body;
            return this;
        }

        public RestClientBuilder body(Object body) {
            this.body = ObjectParser.toJSON(body);
            return this;
        }

        public Map<String, String> getHeaders() {
            if (null == headers) {
                headers = new HashMap<>();
            }
            return headers;
        }

        public RestClientBuilder headers(Map<String, String> headers) {
            this.headers = headers;
            return this;
        }

        public RestClientBuilder addHeader(String key, String value) {
            if (null == this.headers) {
                this.headers = new HashMap<>();
            }
            this.headers.put(key, value);
            return this;
        }

        /**
         * 匹配解析器
         *
         * @return 结果
         */
        private HttpRequestBase buildRequest() {
            HttpRequestBase request = RESOLVER_MAP.getOrDefault(this.method, RESOLVER_MAP.get(HttpMethod.GET))
                    .resolve(this);
            // 添加头
            getHeaders().forEach(request::addHeader);
            // 设置公共设置
            request.setConfig(REQUEST_CONFIG);
            // 返回
            return request;
        }

        /**
         * 构建client
         *
         * @return 结果
         */
        public RestClient build() {
            // 创建请求
            HttpRequestBase request = buildRequest();
            return new RestClient(request);
        }

        /**
         * 存储文件上传的part
         */
        private static class Multipart {

            private String name;

            private String filename;

            private Object data;

            public Multipart(String name, String filename, Object data) {
                this.name = name;
                this.filename = filename;
                this.data = data;
            }
        }
    }

    /**
     * Get方法解析参数的解析器
     */
    private static class HttpGetResolver implements HttpMethodResolver {
        @Override
        public HttpRequestBase resolve(RestClientBuilder builder) {
            if (MapUtils.isNotEmpty(builder.getParams())) {
                StringBuilder paramBuilder = new StringBuilder(builder.getUrl().contains("?") ? "&" : "?");
                builder.getParams().forEach((key, value) ->
                        paramBuilder.append(key).append("=").append(value));
                if (paramBuilder.length() != 1) {
                    paramBuilder.deleteCharAt(paramBuilder.length() - 1);
                }
                builder.url(builder.getUrl() + paramBuilder.toString());
            }
            return new HttpGet(builder.getUrl());
        }
    }

    /**
     * Post方法解析参数的解析器，包括上传
     */
    private static class HttpPostResolver implements HttpMethodResolver {

        @Override
        public HttpRequestBase resolve(RestClientBuilder builder) {
            HttpPost post = new HttpPost(builder.getUrl());
            HttpEntity entity = builder.isMultipart() ? buildMultipart(builder)
                    : StringUtils.isNotBlank(builder.getBody()) ? buildJson(builder)
                    : buildFormData(builder);
            post.setEntity(entity);
            return post;
        }

        /**
         * 构建上传数据
         *
         * @param clientBuilder builder
         * @return 结果
         */
        private HttpEntity buildMultipart(RestClientBuilder clientBuilder) {
            MultipartEntityBuilder builder = MultipartEntityBuilder.create();
            builder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE);
            clientBuilder.getMultipartList().forEach(multipart -> {
                Object data = multipart.data;
                String name = multipart.name;
                String filename = multipart.filename;
                if (data instanceof byte[]) {
                    builder.addBinaryBody(name, (byte[]) data,
                            ContentType.create(MINE_MAP.getOrDefault(FilenameUtils.getExtension(filename),
                                    ContentType.APPLICATION_OCTET_STREAM.getMimeType())), filename);
                } else if (data instanceof File) {
                    builder.addBinaryBody(name, (File) data,
                            ContentType.create(MINE_MAP.getOrDefault(FilenameUtils.getExtension(filename),
                                    ContentType.APPLICATION_OCTET_STREAM.getMimeType())), filename);
                } else {
                    throw new IllegalArgumentException("上传时，输入的数据不被支持！");
                }
            });
            return builder.build();
        }

        /**
         * 构建JSON方式的POST
         *
         * @param clientBuilder builder
         * @return 结果
         */
        private HttpEntity buildJson(RestClientBuilder clientBuilder) {
            clientBuilder.addHeader("Content-Type", "application/json;charset=UTF-8");
            Charset charset = clientBuilder.getCharset();
            StringEntity entity = new StringEntity(clientBuilder.getBody(), charset);
            entity.setContentEncoding(charset.toString());
            entity.setContentType("application/json");
            return entity;
        }

        /**
         * 构建formdata
         *
         * @param clientBuilder builder
         * @return 结果
         */
        private HttpEntity buildFormData(RestClientBuilder clientBuilder) {
            // 设置参数
            Map<String, Object> params = clientBuilder.getParams();
            List<NameValuePair> list = params.keySet()
                    .stream()
                    .filter(key -> null != params.get(key))
                    .map(key -> new BasicNameValuePair(key, String.valueOf(params.get(key))))
                    .collect(Collectors.toList());
            if (CollectionUtils.isNotEmpty(list)) {
                return new UrlEncodedFormEntity(list, clientBuilder.getCharset());
            }
            return null;
        }
    }
}
